Skip to main content

pacenotes_installer\wizard\scene/
download_tree_view.rs

1//! The Download scene's tree view, mirroring `pacenotes_installer::tree::Tree`;
2//! each item's `lParam` is its node index in the model.
3
4use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
5use windows_sys::Win32::UI::Controls as ctl;
6use windows_sys::Win32::UI::WindowsAndMessaging as wm;
7
8const TVM_INSERTITEMW: u32 = 0x1132;
9const TVM_DELETEITEM: u32 = 0x1101;
10const TVM_SETITEMW: u32 = 0x113F;
11const TVM_SETEXTENDEDSTYLE: u32 = 0x112C;
12const TVI_ROOT: isize = -0x10000;
13const TVI_LAST: isize = -0x0FFFE;
14/// `TVN_FIRST - 17`, which windows-sys does not declare.
15pub const TVN_ITEMCHANGEDW: u32 = (-417i32) as u32;
16/// Posted to the page to push the model's checkboxes into the control.
17pub const WM_SYNC_CHECKS: u32 = wm::WM_APP + 10;
18
19/// The state image lives in bits 12-15: 1 unchecked, 2 checked, 3 partial.
20const STATE_IMAGE_SHIFT: u32 = 12;
21const STATE_IMAGE_UNCHECKED: u32 = 1;
22const STATE_IMAGE_CHECKED: u32 = 2;
23const STATE_IMAGE_PARTIAL: u32 = 3;
24
25/// `NMTVITEMCHANGE`, which windows-sys does not declare.
26#[repr(C)]
27struct NmTvItemChange {
28    header: ctl::NMHDR,
29    change: u32,
30    item: *mut core::ffi::c_void,
31    state_new: u32,
32    state_old: u32,
33    lparam: LPARAM,
34}
35
36/// Partial checkboxes are an extended style, which a template cannot request,
37/// and the checkbox images are built when `TVS_CHECKBOXES` arrives: given in
38/// the template, it came first, there was no partial image, and a category
39/// with only some items ticked drew no box at all.
40pub unsafe fn enable_partial_checkboxes(dialog: HWND) {
41    let control = tree_control(dialog);
42    wm::SendMessageW(
43        control,
44        TVM_SETEXTENDEDSTYLE,
45        ctl::TVS_EX_PARTIALCHECKBOXES as WPARAM,
46        ctl::TVS_EX_PARTIALCHECKBOXES as LPARAM,
47    );
48    let style = wm::GetWindowLongPtrW(control, wm::GWL_STYLE);
49    wm::SetWindowLongPtrW(control, wm::GWL_STYLE, style | ctl::TVS_CHECKBOXES as isize);
50}
51
52/// Replaces the control's items with the model's.
53pub unsafe fn populate(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
54    let control = tree_control(dialog);
55    state.tree_syncing = true;
56    wm::SendMessageW(control, TVM_DELETEITEM, 0, TVI_ROOT);
57    state.tree_items = vec![0isize; state.tree.len()];
58
59    for root in state.tree.roots().to_vec() {
60        insert_node(control, state, root, TVI_ROOT);
61    }
62    refresh_checks(dialog, state);
63    state.tree_syncing = false;
64}
65
66unsafe fn insert_node(
67    control: HWND,
68    state: &mut crate::wizard::state::WizardState,
69    node: usize,
70    parent: isize,
71) {
72    let text = crate::i18n::to_wide(&row_label(state, node));
73    let mut item: ctl::TVINSERTSTRUCTW = core::mem::zeroed();
74    item.hParent = parent;
75    item.hInsertAfter = TVI_LAST;
76    item.Anonymous.itemex.mask = ctl::TVIF_TEXT | ctl::TVIF_PARAM | ctl::TVIF_STATE;
77    item.Anonymous.itemex.pszText = text.as_ptr() as *mut u16;
78    item.Anonymous.itemex.lParam = node as LPARAM;
79    // Expanded, so every package is in view without a click; a category
80    // below a long one is a scroll away, which the larger window offsets.
81    item.Anonymous.itemex.stateMask = ctl::TVIS_EXPANDED;
82    item.Anonymous.itemex.state = ctl::TVIS_EXPANDED;
83
84    let handle = wm::SendMessageW(control, TVM_INSERTITEMW, 0, &item as *const _ as LPARAM);
85    state.tree_items[node] = handle;
86    for child in state.tree.children(node).to_vec() {
87        insert_node(control, state, child, handle);
88    }
89}
90
91/// The model's label plus the localized state suffix the model leaves out,
92/// where being in the game folder outranks being in the cache.
93fn row_label(state: &crate::wizard::state::WizardState, node: usize) -> String {
94    let node = state.tree.node(node);
95    let suffix = match (node.installed, node.state) {
96        (_, None) | (false, Some(pacenotes_installer::tree::LeafState::NotCached)) => {
97            return node.label.clone()
98        }
99        (_, Some(pacenotes_installer::tree::LeafState::Unavailable)) => {
100            crate::i18n::IDS_STATE_UNAVAILABLE
101        }
102        (true, Some(_)) => crate::i18n::IDS_STATE_INSTALLED,
103        (false, Some(pacenotes_installer::tree::LeafState::Downloaded)) => {
104            crate::i18n::IDS_STATE_DOWNLOADED
105        }
106        (false, Some(pacenotes_installer::tree::LeafState::UpdateAvailable)) => {
107            crate::i18n::IDS_STATE_UPDATE
108        }
109        (false, Some(pacenotes_installer::tree::LeafState::LocalOnly)) => {
110            crate::i18n::IDS_STATE_LOCAL
111        }
112    };
113    format!("{} {}", node.label, crate::i18n::load_string(suffix))
114}
115
116/// Pushes every node's checkbox from the model into the control.
117pub unsafe fn refresh_checks(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
118    let control = tree_control(dialog);
119    let was_syncing = state.tree_syncing;
120    state.tree_syncing = true;
121    // Off the handle table, since a node the control does not hold has nothing to push.
122    for node in 0..state.tree_items.len().min(state.tree.len()) {
123        let handle = state.tree_items[node];
124        if handle == 0 {
125            continue;
126        }
127        let image = match state.tree.check_of(node) {
128            pacenotes_installer::tree::Check::Unchecked => STATE_IMAGE_UNCHECKED,
129            pacenotes_installer::tree::Check::Checked => STATE_IMAGE_CHECKED,
130            pacenotes_installer::tree::Check::Partial => STATE_IMAGE_PARTIAL,
131        };
132        let mut item: ctl::TVITEMEXW = core::mem::zeroed();
133        item.mask = ctl::TVIF_HANDLE | ctl::TVIF_STATE;
134        item.hItem = handle;
135        item.stateMask = ctl::TVIS_STATEIMAGEMASK;
136        item.state = image << STATE_IMAGE_SHIFT;
137        wm::SendMessageW(control, TVM_SETITEMW, 0, &item as *const _ as LPARAM);
138    }
139    state.tree_syncing = was_syncing;
140}
141
142/// Takes a toggled checkbox into the model, which propagates it through the
143/// subtree, then pushes the whole model back.
144pub unsafe fn on_item_changed(dialog: HWND, lparam: LPARAM) -> isize {
145    let Some(state) = crate::wizard::page::state(dialog) else {
146        return 0;
147    };
148    if state.tree_syncing {
149        return 0;
150    }
151    let Some(change) = (lparam as *const NmTvItemChange).as_ref() else {
152        return 0;
153    };
154    // An item that is not ours carries lParam 0 too, so only the handle table can say it is node 0.
155    let node = change.lparam as usize;
156    if state.tree_items.get(node).copied() != Some(change.item as isize) {
157        return 0;
158    }
159    let image = |state: u32| (state & ctl::TVIS_STATEIMAGEMASK) >> STATE_IMAGE_SHIFT;
160    // The control cycles unchecked, checked, partial; a click on a partly
161    // ticked category means "all of it", as tri-state boxes do elsewhere.
162    let checked = image(change.state_new) == STATE_IMAGE_CHECKED
163        || image(change.state_old) == STATE_IMAGE_PARTIAL;
164    state.tree.set_checked(node, checked);
165    // Posted: written from inside the click, the clicked row would be redrawn with its old state.
166    wm::PostMessageW(dialog, WM_SYNC_CHECKS, 0, 0);
167    0
168}
169
170unsafe fn tree_control(dialog: HWND) -> HWND {
171    wm::GetDlgItem(dialog, crate::wizard::control_ids::IDC_S4_TREE as i32)
172}